Список Задач + Dependency Inversion Principle
➡️Ссылка на репозиторий с кодом этого урока
Модель и UI без изменений

class Task {
int id;
String text;
bool isDone;
Task({required this.id, required this.text, this.isDone = false});
// Фабричный конструктор для создания Task из Map (JSON)
factory Task.fromJson(Map<String, dynamic> json) {
return Task(
id: json['id'],
text: json['text'],
isDone: json['isDone'],
);
}
// Метод для преобразования Task в Map (JSON)
Map<String, dynamic> toJson() {
return {
'id': id,
'text': text,
'isDone': isDone,
};
}
// Метод для копирования объекта с определенными значениями
Task copyWith({
int? id,
String? text,
bool? isDone,
}) {
return Task(
id: id ?? this.id,
text: text ?? this.text,
isDone: isDone ?? this.isDone,
);
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/task.dart';
import '../viewmodels/todo_viewmodel.dart';
class ToDoScreen extends StatelessWidget {
const ToDoScreen({super.key});
@override
Widget build(BuildContext context) {
debugPrint("🔴 ToDoScreen build");
// Следим только за количеством задач
// Чтобы каждый раз не перестраивать ListView final taskCount = context.select<ToDoViewModel, int>(
(vm) => vm.tasks.length,
);
return Scaffold(
appBar: AppBar(
title: const Text('Список Задач'),
actions: [
// Используем Consumer, чтобы лишний раз не перестраивать ListView
Consumer<ToDoViewModel>(
builder: (context, vm, child) => Row(
children: [
Center(
child: Text(vm.isDarkMode ? 'Темная тема' : 'Светлая тема'),
),
Switch(
value: vm.isDarkMode,
// Используем context.read для вызова метода без подписки
onChanged: (_) => context.read<ToDoViewModel>().toggleTheme(),
),
const SizedBox(width: 8),
],
),
),
],
),
body: ListView.builder(
itemCount: taskCount, // Количество задач
itemBuilder: (context, index) {
final tasks = context.read<ToDoViewModel>().tasks;
final task = tasks[index];
return TaskItem(key: ValueKey(task.id), taskId: task.id);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _showAddTaskDialog(context),
label: const Text('Добавить'),
icon: const Icon(Icons.add),
),
);
}
// Показать диалоговое окно
void _showAddTaskDialog(BuildContext context) {
final vm = context.read<ToDoViewModel>();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Добавить задачу'),
content: TextField(
controller: vm.textEditingController,
autofocus: true,
decoration: const InputDecoration(hintText: "Введите текст задачи"),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Отмена'),
),
TextButton(
onPressed: () {
vm.addTask(vm.textEditingController.text);
vm.textEditingController.clear();
Navigator.of(context).pop();
},
child: const Text('Добавить'),
),
],
);
},
);
}
}
/// Карточка задачи
class TaskItem extends StatelessWidget {
final int taskId;
const TaskItem({super.key, required this.taskId});
@override
Widget build(BuildContext context) {
debugPrint("🔴 TaskItem build");
// Используем Selector, чтобы подписаться на изменения ТОЛЬКО одной задачи.
// selector извлекает конкретную задачу по ID.
// builder будет вызван только если объект этой задачи изменился
return Selector<ToDoViewModel, Task>(
// 1. Выбирает конкретный объект Task из списка по заданному ID
selector: (_, viewModel) =>
viewModel.tasks.firstWhere((task) => task.id == taskId),
// 2.Взывается только тогда, когда выбранный Task изменился
builder: (context, task, child) {
// 3. Используется для вызова методов, не подписываясь на обновления.
final vm = context.read<ToDoViewModel>();
return Container(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: ListTile(
leading: Checkbox(
value: task.isDone,
onChanged: (value) {
vm.updateTaskStatus(task.id, value ?? false);
},
),
title: Row(
children: [
Text("${task.id}"),
SizedBox(width: 8),
Text(
task.text,
style: TextStyle(
decoration: task.isDone
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
],
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.cyan),
onPressed: () {
vm.deleteTask(task.id);
},
),
),
);
},
);
}
}